<?php
/**
 * 比较两个数据库表的差异, 并自动生成同步SQL
 */
class DBcompare
{
    public $srcDbHost = '';
    public $targetDbHost = '';
	public $exportTables = array(); // 要导出的表
    public $srcDbName = '';
    public $targetDbName = '';
    
    public $srcTables = array();
    public $targetTables = array();
    
    public $srcDbInfo = array();
    public $targetDbInfo = array();
    
    public $srcLink = NULL;
    public $targetLink = NULL;
    
    public $diffTables = array();
    public $diffColumns = array();
    public $diffIndex = array();
    
    public $createTable = array();
    public $alterTable = array();
    public $alterIndex = array();
    
    public $addData = array(); //相同表多出的数据
    public $diffData = array();//A字段值相同, 但B字段值不同的记录
    
    public $matchField = array(); //包含某字符串的字段名
    public $filterTable = array(); //被过滤掉的表
    
    //mysql保留函数 https://dev.mysql.com/doc/refman/8.0/en/func-op-summary-ref.html
    public $keywords = array(
        'NULL','ABS', 'ACOS', 'ADDDATE', 'ADDTIME', 'AES_DECRYPT', 'AES_ENCRYPT', 'AND', 'ANY_VALUE', 'ASCII', 'ASIN',
        'ASYMMETRIC_DECRYPT', 'ASYMMETRIC_DERIVE', 'ASYMMETRIC_ENCRYPT', 'ASYMMETRIC_SIGN', 'ASYMMETRIC_VERIFY', 'ATAN',
        'ATAN2', 'AVG', 'BENCHMARK', 'BIN', 'BIN_TO_UUID', 'BINARY', 'BIT_AND', 'BIT_COUNT', 'BIT_LENGTH', 'BIT_OR', 'BIT_XOR',
        'CAN_ACCESS_COLUMN', 'CAN_ACCESS_DATABASE', 'CAN_ACCESS_TABLE', 'CAN_ACCESS_VIEW', 'CASE ', 'CAST', 'CEIL', 'CEILING',
        'CHAR', 'CHAR_LENGTH', 'CHARACTER_LENGTH', 'CHARSET', 'COALESCE', 'COERCIBILITY', 'COLLATION', 'COMPRESS', 'CONCAT',
        'CONCAT_WS', 'CONNECTION_ID', 'CONV', 'CONVERT', 'CONVERT_TZ', 'COS', 'COT', 'COUNT', 'CRC32', 'CREATE_ASYMMETRIC_PRIV_KEY',
        'CREATE_ASYMMETRIC_PUB_KEY', 'CREATE_DH_PARAMETERS', 'CREATE_DIGEST', 'CUME_DIST', 'CURDATE', 'CURRENT_DATE', 'CURRENT_ROLE',
        'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'CURRENT_USER', 'CURTIME', 'DATABASE', 'DATE', 'DATE_ADD', 'DATE_FORMAT', 'DATE_SUB', 'DATEDIFF',
        'DAY', 'DAYNAME', 'DAYOFMONTH', 'DAYOFWEEK', 'DAYOFYEAR', 'DEFAULT', 'DEGREES', 'DENSE_RANK', 'DIV', 'ELT', 'EXP', 'EXPORT_SET',
        'EXTRACT', 'ExtractValue', 'FIELD', 'FIND_IN_SET', 'FIRST_VALUE', 'FLOOR', 'FORMAT', 'FORMAT_BYTES', 'FORMAT_PICO_TIME', 'FOUND_ROWS',
        'FROM_BASE64', 'FROM_DAYS', 'FROM_UNIXTIME', 'GeomCollection', 'GeometryCollection', 'GET_DD_COLUMN_PRIVILEGES', 'GET_DD_CREATE_OPTIONS',
        'GET_DD_INDEX_SUB_PART_LENGTH', 'GET_FORMAT', 'GET_LOCK', 'GREATEST', 'GROUP_CONCAT', 'GROUPING', 'GTID_SUBSET', 'GTID_SUBTRACT', 'HEX',
        'HOUR', 'ICU_VERSION', 'IF', 'IFNULL', 'IN', 'INET_ATON', 'INET_NTOA', 'INET6_ATON', 'INET6_NTOA', 'INSERT', 'INSTR', 'INTERNAL_AUTO_INCREMENT',
        'INTERNAL_AVG_ROW_LENGTH', 'INTERNAL_CHECK_TIME', 'INTERNAL_CHECKSUM', 'INTERNAL_DATA_FREE', 'INTERNAL_DATA_LENGTH', 'INTERNAL_DD_CHAR_LENGTH',
        'INTERNAL_GET_COMMENT_OR_ERROR', 'INTERNAL_GET_ENABLED_ROLE_JSON', 'INTERNAL_GET_HOSTNAME', 'INTERNAL_GET_USERNAME',
        'INTERNAL_GET_VIEW_WARNING_OR_ERROR', 'INTERNAL_INDEX_COLUMN_CARDINALITY', 'INTERNAL_INDEX_LENGTH', 'INTERNAL_IS_ENABLED_ROLE',
        'INTERNAL_IS_MANDATORY_ROLE', 'INTERNAL_KEYS_DISABLED', 'INTERNAL_MAX_DATA_LENGTH', 'INTERNAL_TABLE_ROWS', 'INTERNAL_UPDATE_TIME',
        'INTERVAL', 'IS_FREE_LOCK', 'IS_IPV4', 'IS_IPV4_COMPAT', 'IS_IPV4_MAPPED', 'IS_IPV6', 'IS NOT', 'IS NOT NULL ', 'IS NULL',
        'IS_USED_LOCK', 'IS_UUID', 'ISNULL', 'JSON_ARRAY', 'JSON_ARRAY_APPEND', 'JSON_ARRAY_INSERT', 'JSON_ARRAYAGG', 'JSON_CONTAINS',
        'JSON_CONTAINS_PATH', 'JSON_DEPTH', 'JSON_EXTRACT', 'JSON_INSERT', 'JSON_KEYS', 'JSON_LENGTH', 'JSON_MERGE', 'JSON_MERGE_PATCH',
        'JSON_MERGE_PRESERVE', 'JSON_OBJECT', 'JSON_OBJECTAGG', 'JSON_OVERLAPS', 'JSON_PRETTY', 'JSON_QUOTE', 'JSON_REMOVE', 'JSON_REPLACE',
        'JSON_SCHEMA_VALID', 'JSON_SCHEMA_VALIDATION_REPORT', 'JSON_SEARCH', 'JSON_SET', 'JSON_STORAGE_FREE', 'JSON_STORAGE_SIZE', 'JSON_TABLE',
        'JSON_TYPE', 'JSON_UNQUOTE', 'JSON_VALID', 'JSON_VALUE', 'LAG', 'LAST_DAY', 'LAST_INSERT_ID', 'LAST_VALUE', 'LCASE', 'LEAD', 'LEAST', 'LEFT',
        'LENGTH', 'LIKE', 'LineString', 'LN', 'LOAD_FILE', 'LOCALTIME', 'LOCALTIMESTAMP', 'LOCATE', 'LOG', 'LOG10', 'LOG2', 'LOWER', 'LPAD', 'LTRIM',
        'MAKE_SET', 'MAKEDATE', 'MAKETIME', 'MASTER_POS_WAIT', 'MATCH ', 'MAX', 'MBRContains', 'MBRCoveredBy', 'MBRCovers', 'MBRDisjoint', 'MBREquals',
        'MBRIntersects', 'MBROverlaps', 'MBRTouches', 'MBRWithin', 'MD5', 'MEMBER OF', 'MICROSECOND', 'MID', 'MIN', 'MINUTE', 'MOD', 'MONTH', 'MONTHNAME',
        'MultiLineString', 'MultiPoint', 'MultiPolygon', 'NAME_CONST', 'NOT', 'NOT IN', 'NOT LIKE', 'NOT REGEXP', 'NOW', 'NTH_VALUE', 'NTILE', 'NULLIF',
        'OCT', 'OCTET_LENGTH', 'OR, || 	Logical OR', 'ORD', 'PERCENT_RANK', 'PERIOD_ADD', 'PERIOD_DIFF', 'PI', 'Point', 'Polygon', 'POSITION', 'POW',
        'POWER', 'PS_CURRENT_THREAD_ID', 'PS_THREAD_ID', 'QUARTER', 'QUOTE', 'RADIANS', 'RAND', 'RANDOM_BYTES', 'RANK', 'REGEXP', 'REGEXP_INSTR',
        'REGEXP_LIKE', 'REGEXP_REPLACE', 'REGEXP_SUBSTR', 'RELEASE_ALL_LOCKS', 'RELEASE_LOCK', 'REPEAT', 'REPLACE', 'REVERSE', 'RIGHT', 'RLIKE',
        'ROLES_GRAPHML', 'ROUND', 'ROW_COUNT', 'ROW_NUMBER', 'RPAD', 'RTRIM', 'SCHEMA', 'SEC_TO_TIME', 'SECOND', 'SESSION_USER', 'SHA1', 'SHA2',
        'SIGN', 'SIN', 'SLEEP', 'SOUNDEX', 'SOUNDS LIKE', 'SPACE', 'SQRT', 'ST_Area', 'ST_AsBinary', 'ST_AsGeoJSON', 'ST_AsText', 'ST_Buffer',
        'ST_Buffer_Strategy', 'ST_Centroid', 'ST_Contains', 'ST_ConvexHull', 'ST_Crosses', 'ST_Difference', 'ST_Dimension', 'ST_Disjoint', 'ST_Distance',
        'ST_Distance_Sphere', 'ST_EndPoint', 'ST_Envelope', 'ST_Equals', 'ST_ExteriorRing', 'ST_GeoHash', 'ST_GeomCollFromText', 'ST_GeomCollFromWKB',
        'ST_GeometryN', 'ST_GeometryType', 'ST_GeomFromGeoJSON', 'ST_GeomFromText', 'ST_GeomFromWKB', 'ST_InteriorRingN', 'ST_Intersection',
        'ST_Intersects', 'ST_IsClosed', 'ST_IsEmpty', 'ST_IsSimple', 'ST_IsValid', 'ST_LatFromGeoHash', 'ST_Latitude', 'ST_Length', 'ST_LineFromText',
        'ST_LineFromWKB', 'ST_LongFromGeoHash', 'ST_Longitude', 'ST_MakeEnvelope', 'ST_MLineFromText', 'ST_MLineFromWKB', 'ST_MPointFromText',
        'ST_MPointFromWKB', 'ST_MPolyFromText', 'ST_MPolyFromWKB', 'ST_NumGeometries', 'ST_NumInteriorRing', 'ST_NumPoints', 'ST_Overlaps',
        'ST_PointFromGeoHash', 'ST_PointFromText', 'ST_PointFromWKB', 'ST_PointN', 'ST_PolyFromText', 'ST_PolyFromWKB', 'ST_Simplify', 'ST_SRID',
        'ST_StartPoint', 'ST_SwapXY', 'ST_SymDifference', 'ST_Touches', 'ST_Transform', 'ST_Union', 'ST_Validate', 'ST_Within', 'ST_X', 'ST_Y',
        'STATEMENT_DIGEST', 'STATEMENT_DIGEST_TEXT', 'STD', 'STDDEV', 'STDDEV_POP', 'STDDEV_SAMP', 'STR_TO_DATE', 'STRCMP', 'SUBDATE', 'SUBSTR',
        'SUBSTRING', 'SUBSTRING_INDEX', 'SUBTIME', 'SUM', 'SYSDATE', 'SYSTEM_USER', 'TAN', 'TIME', 'TIME_FORMAT', 'TIME_TO_SEC', 'TIMEDIFF', 'TIMESTAMP',
        'TIMESTAMPADD', 'TIMESTAMPDIFF', 'TO_BASE64', 'TO_DAYS', 'TO_SECONDS', 'TRIM', 'TRUNCATE', 'UCASE', 'UNCOMPRESS', 'UNCOMPRESSED_LENGTH', 'UNHEX',
        'UNIX_TIMESTAMP', 'UpdateXML', 'UPPER', 'USER', 'UTC_DATE', 'UTC_TIME', 'UTC_TIMESTAMP', 'UUID', 'UUID_SHORT', 'UUID_TO_BIN',
        'VALIDATE_PASSWORD_STRENGTH', 'VALUES', 'VAR_POP', 'VAR_SAMP', 'VARIANCE', 'VERSION', 'WAIT_FOR_EXECUTED_GTID_SET',
        'WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS', 'WEEK', 'WEEKDAY', 'WEEKOFYEAR', 'WEIGHT_STRING', 'XOR', 'YEAR', 'YEARWEEK',
        );
    
    public $error = array();
    
	public static function ini()
	{
		return new self();
	}
	
    function __construct()
    {
        $this->srcDbInfo['tables'] = array();
        $this->srcDbInfo['columns'] = array();
        $this->srcDbInfo['columns_detail'] = array(); //用来生成alter table add 语句
        $this->srcDbInfo['index'] = array();
        $this->srcDbInfo['index_detail'] = array();
    
        $this->targetDbInfo['tables'] = array();
        $this->targetDbInfo['columns'] = array();
        $this->targetDbInfo['index'] = array();
    }
    
    /**
     * @param string $role 可选值 src: 源数据库 target: 目标数据库
     * @param $host
     * @param $user
     * @param $password
     * @param $dbname
     * @param string $port
     *
     * @return $this
     */
	public function build($role, $host, $user, $password, $dbname, $port='3306')
	{
		//链接MySQL
        $mysqli = mysqli_init();
        $mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 2); //超时2s
        $mysqli->options(MYSQLI_INIT_COMMAND, "set names utf8mb4;");
        $mysqli->real_connect($host, $user, $password, $dbname, $port) or exit("Mysql connect is error. {$mysqli->error}");
        
        if ($role == 'src') {
            $this->srcDbHost = $host;
            $this->srcDbName = $dbname;
            $this->srcLink = $mysqli;
        } elseif ($role == 'target') {
            $this->targetDbHost = $host;
            $this->targetDbName = $dbname;
            $this->targetLink = $mysqli;
        }
        
        // 取得所有表名
        $rs = $mysqli->query('show tables');
        $arrTableName = array_column($rs->fetch_all(), $value=0);

        // 取得所有符合条件的表信息
        foreach ($arrTableName as $tname) {
            $isExport = $this->isNeedExport($tname);
			
			if ($isExport == FALSE) {
			    $this->filterTable[] = $tname;
			    continue;
            }
        
            //表注释
            $sql = "select * from information_schema.tables where table_schema = '{$dbname}' and table_name = '{$tname}' "; //查询表信息
            $rs = $mysqli->query($sql);
            $arrTableInfo = $rs->fetch_assoc();

            //各字段详细信息
            $sql = "select * from information_schema.columns where table_schema ='{$dbname}' and table_name = '{$tname}' "; //查询字段信息
            $rs = $mysqli->query($sql);
            $arrColumnInfo = $rs->fetch_all(MYSQLI_ASSOC);
            
            $columns = array();
            foreach ($arrColumnInfo as $columnInfo) {
                $columns[$columnInfo['COLUMN_NAME']] = $columnInfo;
            }
            unset($arrColumnInfo);
            
            //索引信息
            $sql = "show index from {$tname}";
            $rs = $mysqli->query($sql);
			if (!empty($rs->num_rows)) {
				$arrIndexInfo = $rs->fetch_all(MYSQLI_ASSOC);
                $indexDetail = array();
				foreach ($arrIndexInfo as $index) {
				    $indexDetail[$index['Key_name']][$index['Seq_in_index']] = $index;
                }
                unset($arrIndexInfo);
			} else {
                $indexDetail = array();
			}
   
			$columnNames = array_keys($columns);
			$indexNames = array_keys($indexDetail);
            //sort($columnNames);
    
            $tableName = $arrTableInfo['TABLE_NAME'];
            $tableType = $arrTableInfo['TABLE_TYPE']; //普通表: BASE TABLE 视图: VIEW
            if ($role == 'src') {
                $this->srcDbInfo['tables'][$tableName] = array('name' => $tableName, 'type' => $tableType);
                $this->srcDbInfo['columns'][$tableName] = $columnNames;
                $this->srcDbInfo['columns_detail'][$tableName] = $columns; //用来生成alter table add 语句
                $this->srcDbInfo['index'][$tableName] = $indexNames;
                $this->srcDbInfo['index_detail'][$tableName] = $indexDetail;
                $this->srcTables[] = $tableName;
                unset($columns);
            } elseif ($role == 'target') {
                $this->targetDbInfo['tables'][$tableName] = array('name' => $tableName, 'type' => $tableType);;
                $this->targetDbInfo['columns'][$tableName] = $columnNames;
                $this->targetDbInfo['index'][$tableName] = $indexNames;
                $this->targetTables[] = $tableName;
            }
        }
		return $this;
	}
	
	//设置需要导出的表, 参数为单个表
    public function setExportTable($tableName)
	{
		$this->exportTables[] = $tableName;
		return $this;
	}
	
	//设置需要导出的表, 参数为数组
	public function setExportTableArray($arrTableName)
	{
		$this->exportTables = array_merge($this->exportTables, $arrTableName);
		return $this;
	}
	
	//检查某张表是否需要参与对比
	public function isNeedExport($tname)
    {
        if (!empty($this->exportTables)) {
            
            foreach ($this->exportTables as $reg) {
                if (preg_match("/$reg/i", $tname, $matches) == 0) {
                    //echo $tname.'<br>';
                    return FALSE;
                }
            }
            
            return TRUE;
        } else {
            return TRUE;
        }
    }
	
    //获取表, 字段, 索引的不同
    public function getDiff()
    {
        //获取src有, 但是target没有的表
        $this->diffTables = array_diff_key($this->srcDbInfo['tables'], $this->targetDbInfo['tables']);

        //获取src有, 但是target没有的字段
        foreach ($this->srcDbInfo['columns'] as $tableName=>$arrColumn) {
            if (!empty($this->targetDbInfo['columns'][$tableName])) {
                $diff = array_diff($arrColumn, $this->targetDbInfo['columns'][$tableName]);
                !empty($diff) && $this->diffColumns[$tableName] = $diff;
            }
        }
        
        //获取src有, 但是target没有的索引
        foreach ($this->srcDbInfo['index'] as $tableName=>$index) {
            if (!empty($this->targetDbInfo['tables'][$tableName])) {
                $diff = array_diff($index, $this->targetDbInfo['index'][$tableName]);
                !empty($diff) && $this->diffIndex[$tableName] = $diff;
            }
        }
        
        return $this;
    }
    
    //根据不同, 创建SQL语句
    public function createSQL()
    {
        //创建表的SQL
        foreach ($this->diffTables as $table => $tableInfo) {
            if ($tableInfo['type'] == 'BASE TABLE') {
                $sql = "show create table {$table}";
            } elseif ($tableInfo['type'] == 'VIEW') {
                $sql = "show create view {$table}";
            } else {
                $sql = '';
            }
            
            $rs = $this->srcLink->query($sql);
            if (!empty($rs)) { //可能会因为权限问题导致不能执行成功
                $info = $rs->fetch_assoc();
                if (!empty($info['Create Table'])) {
                    $this->createTable[$table] = $info['Create Table'].';';
                } else {
                    // Create View
                }
                
            } else {
                $this->error[] = $sql;
            }
        }
        
        //添加字段
        foreach ($this->diffColumns as $table=>$fields) {
            foreach ($fields as $field) {
                $info = $this->srcDbInfo['columns_detail'][$table][$field];

                $sql = "ALTER TABLE `{$table}` ADD {$field} {$info['COLUMN_TYPE']}";
                
                //not null
                if ($info['IS_NULLABLE'] == 'NO') {
                    $sql .= ' NOT NULL ';
                } 

                //default
                if (is_null($info['COLUMN_DEFAULT'])) {
                    $sql .= ' DEFAULT NULL ';
                } elseif (is_string($info['COLUMN_DEFAULT'])) {
                    if (strlen($info['COLUMN_DEFAULT']) == 0) {
                        $sql .= " DEFAULT '' ";
                    } elseif (in_array($info['COLUMN_DEFAULT'], $this->keywords, true)) {
                        $sql .= " DEFAULT {$info['COLUMN_DEFAULT']} ";
                    } else {
                        $sql .= " DEFAULT '{$info['COLUMN_DEFAULT']}' ";
                    }
                } elseif (is_numeric($info['COLUMN_DEFAULT'])) {
                    $sql .= " DEFAULT {$info['COLUMN_DEFAULT']} ";
                }
                
                //extra on update CURRENT_TIMESTAMP
                if (!empty($info['EXTRA'])) {
                    $sql .= " {$info['EXTRA']} ";
                }
                
                if (!empty($info['COLUMN_COMMENT'])) {
                    $sql .= " COMMENT \"{$info['COLUMN_COMMENT']}\" ";
                }
                
                $sql .= ';';
                
                $this->alterTable[$table][$field] = $sql;
            }
        }
        
        //添加索引
        foreach ($this->diffIndex as $table=>$index) {
            foreach ($index as $indexName) {
                $indexInfo = $this->srcDbInfo['index_detail'][$table][$indexName];
                
                if ($indexName == 'PRIMARY') {
                    $sql = "ALTER TABLE {$table} ADD PRIMARY KEY ({$indexInfo['Column_name']});" ;
                } else {
                    $arrCols = array();
                    foreach ($indexInfo as $in) {
                        $arrCols[] = $in['Column_name'];
                    }
                    $strCols = implode("`,`", $arrCols);
                    
                    if ($indexInfo[1]['Non_unique'] == 0) {
                        $sql = "ALTER TABLE {$table} ADD UNIQUE INDEX `$indexName`(`{$strCols}`);";
                    } else {
                        $sql = "ALTER TABLE {$table} ADD INDEX `$indexName`(`{$strCols}`);";
                    }
                }
    
                $this->alterIndex[$table][$indexName] = $sql;
            }
        }
        
        return $this;
    }
    
    private function getTableHtml()
    {
        $table = '';
    
        //创建表
        if (!empty($this->createTable)) {
            $table = '<table align="center">';
            $table .= '<caption><h3>添加表</h3></caption>';
            $table .= '<tbody><tr><th>表名</th><th>SQL</th></tr>';
        
            foreach ($this->createTable as $tname=>$sql) {
                $sql = nl2br($sql);
                $table .= "<tr><td>{$tname}</td><td>{$sql}</td></tr>";
            }
        
            $table .= '</tbody></table><br><br>';
        }
    
        //添加字段
        if (!empty($this->alterTable)) {
            $table .= '<table align="center">';
            $table .= '<caption><h3>添加字段</h3></caption>';
            $table .= '<tbody><tr><th>表名</th><th>SQL</th></tr>';
        
            foreach ($this->alterTable as $tname=>$fields) {
                $sqls = implode('<br>', $fields);
                $table .= "<tr><td>{$tname}</td><td>{$sqls}</td></tr>";
            }
        
            $table .= '</tbody></table><br><br>';
        }
    
        //添加索引
        if (!empty($this->alterIndex)) {
            $table .= '<table align="center">';
            $table .= '<caption><h3>添加索引</h3></caption>';
            $table .= '<tbody><tr><th>表名</th><th>SQL</th></tr>';
        
            foreach ($this->alterIndex as $tname=>$index) {
                $sqls = implode('<br>', $index);
                $table .= "<tr><td>{$tname}</td><td>{$sqls}</td></tr>";
            }
        
            $table .= '</tbody></table><br><br>';
        }
    
        if (!empty($this->addData)) {
            $table .= '<table align="center">';
            $table .= '<caption><h3>添加数据</h3></caption>';
            $table .= '<tbody><tr><th>表名</th><th>SQL</th></tr>';
        
            foreach ($this->addData as $tname=>$sql) {
                $table .= "<tr><td>{$tname}</td><td>{$sql}</td></tr>";
            }
        
            $table .= '</tbody></table><br><br>';
        }
    
        if (!empty($this->diffData)) {
            foreach ($this->diffData as $tname=>$data) {
                $filedSame = $data['same'];
                $fieldDiff = $data['diff'];
                $table .= '<table align="center">';
                $table .= "<caption><h3> {$tname} 表 {$filedSame} 相同但 {$fieldDiff} 不同</h3></caption>";
                $table .= "<tbody><tr><th>{$filedSame}</th><th>{$this->srcDbName}.[{$fieldDiff}]</th><th>{$this->targetDbName}.[{$fieldDiff}]</th><th>sql</th></tr>";
            
                foreach ($data['src'] as $k=>$v) {
                    $target = $data['target'][$k];
                    //$sql = "update {$tname} set $fieldDiff = $target where $filedSame = '$k';";
                    $sql = '--';
                    if ($target != $v) {
                        $table .= "<tr><td>{$k}</td><td>{$v}</td><td>{$target}</td><td>{$sql}</td></tr>";
                    }
                }
            
                $table .= '</tbody></table><br><br>';
            }
        }
    
        if (!empty($this->matchField)) {
            $table .= '<table align="center">';
            $table .= '<caption><h3>匹配的字段名</h3></caption>';
            $table .= '<tbody><tr><th>表名</th><th>src 字段</th><th>target 字段</th></tr>';
    
            ksort($this->matchField['src']);
            foreach ($this->matchField['src'] as $tname=>$fields) {
                $str1 = implode(',', $fields);
                $str2 = '';
                if (!empty($this->matchField['target'][$tname])) {
                    $str2 = implode(',', $this->matchField['target'][$tname]);
                }
                $table .= "<tr><td>{$tname}</td><td>{$str1}</td><td>{$str2}</td></tr>";
            }
    
            $table .= '</tbody></table><br><br>';
            
        }
    
        if (!empty($this->filterTable)) {
            $table .= '<table align="center">';
            $table .= '<caption><h3>被过滤的表</h3></caption>';
            $table .= '<tbody><tr><th>表名</th></tr>';
        
            ksort($this->filterTable);
            $str = implode(',', $this->filterTable);
            
            $table .= "<tr><td>{$str}</td></tr>";
            $table .= '</tbody></table><br><br>';
        
        }
    
        return $table;
    }
    
    //输出到浏览器, 表格宽度用百分比
    public function outForBrowser()
    {
        $table = $this->getTableHtml();
        
		header("Content-type:text/html;charset=utf-8");
        $html = '<html>
              <meta charset="utf-8">
              <title>数据库对比</title>
              <style>
				table { width: 75%; font-family: Consolas,verdana,arial; font-size:14px; color:#333333; border-width: 1px; border-color: #ddd; border-collapse: collapse; margin-bottom: 5px; }
				table caption { text-align:left; }
				table caption h3 {margin:5px}
				table th { border-width: 1px; padding: 8px; border-style: solid; border-color: #ddd; background-color: #f8f8f8; }
				table td { border-width: 1px; padding: 8px; border-style: solid; border-color: #ddd; background-color: #ffffff; max-width: 1000px; word-break: normal; word-wrap: break-word}
				tr:hover td{ background-color:#f1f5fb; }
              </style>
              <body>';
        $html .= '<h1 style="text-align:center;">数据库对比</h1>';
        $html .= '<p style="text-align:center;margin:20px auto;">'."{$this->srcDbHost}::{$this->srcDbName} => {$this->targetDbHost}::{$this->targetDbName}". '</p>';
        $html .= '<p style="text-align:center;margin:20px auto;">生成时间：' . date('Y-m-d H:i:s') . '</p>';
        $html .= $table;
        $html .= '</body></html>';
		
		echo $html;
		// return $this;
    }
    
    //找出两个库中的相同表的不同数据, 多个字段时,$field用逗号隔开,不能有空格
    public function diffData($table, $field)
    {
        //目标表中已有的 $field
        $sql = "select {$field} from {$table} order by {$field}";
        $rs = $this->srcLink->query($sql);
        $arr = $rs->fetch_all(MYSQLI_ASSOC);
        unset($rs);
        $list1 = array();
        foreach ($arr as $v) {
            $list1[] = implode(',', $v);
        }
        

        //源表中多出的 $field
        $rs = $this->targetLink->query($sql);
        $arr = $rs->fetch_all(MYSQLI_ASSOC);
        unset($rs);
        $list2 = array();
        foreach ($arr as $v) {
            $list2[] = implode(',', $v);
        }
        
        //找出不同的值
        $diff = array_diff($list1, $list2);
        unset($list1, $list2);
        
        $list = array();
        $arrField = explode(',', $field);
        foreach ($diff as $v) {
            $tmp = explode(',', $v);
            foreach ($tmp as $kk => $vv) {
                $f = $arrField[$kk]; //字段名
                $list[$f][$vv] = $vv; //值, 去重
            }
        }
        
        $arrWhere = array();
        foreach ($list as $f => $fv) {
            $str = implode("','", $fv);
            $arrWhere[] = "{$f} in ('{$str}')";
        }
        unset($list);
        
        $strWhere = implode(' and ', $arrWhere);
        $sql = "select * from {$table} where {$strWhere};";
        
        $this->addData[$table] = $sql;
        
        return $this;
    }
    
    //找到表中字段fieldSame相同, 但是$fieldDiff不同的记录
    public function diffDataByField($table, $fieldSame, $fieldDiff, $where='1=1')
    {
        //目标表中已有的 $field
        $sql = "select {$fieldSame},{$fieldDiff} from {$table} where {$where} order by {$fieldSame}";
        $arrFieldSame = array_flip(explode(',', $fieldSame));
        $arrFieldDiff = array_flip(explode(',', $fieldDiff));
    
        //源表中的数据
        $rs = $this->srcLink->query($sql);
        $arr = $rs->fetch_all(MYSQLI_ASSOC);
        unset($rs);
        $list1 = array();
        foreach ($arr as $v) {
            $same = array_intersect_key($v, $arrFieldSame);
            $diff = array_intersect_key($v, $arrFieldDiff);
            $strSame = implode(',', $same);
            $strDiff = implode(',', $diff);
            $list1[$strSame] = $strDiff;
        }
    
        //目标表中的数据
        $rs = $this->targetLink->query($sql);
        $arr = $rs->fetch_all(MYSQLI_ASSOC);
        unset($rs);
        $list2 = array();
        foreach ($arr as $v) {
            $same = array_intersect_key($v, $arrFieldSame);
            $diff = array_intersect_key($v, $arrFieldDiff);
            $strSame = implode(',', $same);
            $strDiff = implode(',', $diff);
            $list2[$strSame] = $strDiff;
        }
        
        //找出相同的键
        $diff = array_intersect_key($list1, $list2);
        
        $diff1 = array_intersect_key($list1, $diff); unset($list1);
        $diff2 = array_intersect_key($list2, $diff); unset($list2);
        
        $this->diffData[$table] = array('same' => $fieldSame, 'diff' => $fieldDiff, 'src' => $diff1, 'target' => $diff2);
        
        return $this;
    }
    
    public function diffSchema()
    {
        $this->getDiff();
        $this->createSQL();
        return $this;
    }
    
    //获取表中有某个字符串的表名
    public function matchField($field)
    {
        //$this->matchField['src'] = [];
        foreach ($this->srcDbInfo['columns'] as $tabelName => $columns) {
            foreach ($columns as $column) {
                if (strpos($column, $field) !== false) {
                    $this->matchField['src'][$tabelName][] = $column;
                }
            }
        }
    
        //$this->matchField['target'] = [];
        foreach ($this->targetDbInfo['columns'] as $tabelName => $columns) {
            foreach ($columns as $column) {
                if (strpos($column, $field) !== false) {
                    $this->matchField['target'][$tabelName][] = $column;
                }
            }
        }
        
        return $this;
    }
	
    //调试用
	function out()
	{
	    echo '<pre>';
//	    print_r($this->srcDbHost);
//	    print_r('<br>');
//	    print_r($this->targetDbHost);
//	    print_r($this->srcTables);
//	    print_r($this->targetTables);
//		print_r($this->srcDbInfo);
//		print_r($this->srcDbInfo['columns_detail']);
//		print_r($this->targetDbInfo);
       print_r($this->diffTables);
//        print_r($this->diffColumns);
//        print_r($this->createTable);
//        print_r($this->alterTable);
        
//        print_r($this->diffIndex);
       
	}
    
}

